How metric recording works with diginsight and Opentelemetry
π Table of Contents
- Understanding Diginsight Metrics
- What are Diginsight Metrics?
- Configuration Architecture: Shared and Metric-Specific Settings
- Tags and Low Cardinality
- Filtering Metrics with IMetricRecordingFilter
- Enriching Metrics with IMetricRecordingEnricher
- Custom Metric Recorders
- Complete Configuration Example
- References
Understanding Diginsight Metrics
Diginsight provides automatic span duration metrics collection that seamlessly integrates with OpenTelemetry. These metrics measure the execution time of operations (activities/spans) throughout your application.
What are Diginsight Metrics?
Diginsight metrics are automatically collected through MetricRecorder classes, with the primary recorder being SpanDurationMetricRecorder. This recorder:
- Listens to activity lifecycle events using the .NET
ActivityListenermechanism - Automatically records duration when activities complete
- Exports metrics via OpenTelemetry to your monitoring backend (Application Insights, Prometheus, etc.)
Key metric: - diginsight.span_duration: Records the duration in milliseconds of each activity/span with tags like: - span_name: The operation name - status: Activity status (Ok, Error, etc.) - Custom tags from activity attributes
Example:
public async Task<Order> ProcessOrderAsync(int orderId)
{
// Diginsight automatically creates instrumented activity
using var activity = ActivitySource.StartMethodActivity(new { orderId });
// Your business logic executes
var order = await GetOrderFromDatabase(orderId);
await ValidateInventory(order);
await ProcessPayment(order);
// when activity stops, metrics are automatically recorded:
// diginsight.span_duration{span_name="ProcessOrderAsync", status="Ok", orderId="123"} = 250ms
return order;
}How Metrics Are Sent
Metrics flow through this pipeline:
Activity Creation β ActivityStopped Event β SpanDurationMetricRecorder
β OpenTelemetry Metrics Pipeline β Exporters (App Insights, Prometheus, etc.)
Startup Configuration:
// Register Diginsight metrics collection
builder.Services.AddSpanDurationMetricRecorder();
// Configure OpenTelemetry to export metrics
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => metrics
.AddMeter("Diginsight.Diagnostics") // Listen to Diginsight metrics
.AddPrometheusExporter() // Export to Prometheus
.AddApplicationInsightsExporter()); // Export to AzureFiltering Metrics with IMetricRecordingFilter
Not all activities need metrics. Filtering reduces costs and noise by recording only relevant operations.
The IMetricRecordingFilter Interface
public interface IMetricRecordingFilter
{
bool? ShouldRecord(Activity activity, Instrument instrument);
}Return values: - true: Force recording - false: Prevent recording - null: Defer to default configuration
OptionsBasedMetricRecordingFilter
Filters activities based on patterns in configuration.
Configuration:
{
"OptionsBasedMetricRecordingFilter": {
"ActivityNames": {
"MyApp.Orders.*": true, // Record all order operations
"MyApp.Database.*": true, // Record database operations
"Microsoft.AspNetCore.*": false, // Exclude framework activities
"System.*": false // Exclude system activities
}
}
}Registration:
builder.Services.AddSingleton<IMetricRecordingFilter, OptionsBasedMetricRecordingFilter>();How it works: 1. When an activity stops, the filter checks its Source.Name and OperationName 2. Matches against configured patterns using wildcards (*) 3. Returns true (record) or false (skip) based on first match
HttpHeadersSpanDurationMetricRecordingFilter
Enables dynamic filtering via HTTP headers - perfect for production debugging!
Use case: Enable metrics for specific requests without redeploying:
# Send request with header to enable metrics for this request
curl -H "Activity-Span-Recording: true" https://myapi.com/api/orders/123How it works:
public class HttpHeadersSpanDurationMetricRecordingFilter : IMetricRecordingFilter
{
public const string HeaderName = "Activity-Span-Recording";
public virtual bool? ShouldRecord(Activity activity, Instrument instrument)
{
// Check if current HTTP request has the special header
var httpContext = httpContextAccessor.HttpContext;
if (httpContext?.Request.Headers.TryGetValue(HeaderName, out var value) == true)
{
return bool.TryParse(value, out var result) && result;
}
return null; // Defer to other filters
}
}Registration:
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<IMetricRecordingFilter, HttpHeadersSpanDurationMetricRecordingFilter>();Benefits: - β On-demand metrics for troubleshooting specific requests - β No deployment required - toggle via HTTP headers - β Safe for production - only affects requests with the header - β Fine-grained control - per-request basis
Enriching Metrics with IMetricRecordingEnricher
Enrichers add contextual tags to metrics automatically by extracting relevant information from activities and their context.
The IMetricRecordingEnricher Interface
public interface IMetricRecordingEnricher
{
Tags ExtractTags(Activity activity, Instrument instrument);
}Purpose: Extract business-relevant tags from activities to enrich metrics.
Parameters: - activity: The activity that just completed - contains operation context and tags - instrument: The specific metric instrument (e.g., Histogram<double>) being recorded to - enables instrument-specific tag extraction
Return Type: Tags (alias for IEnumerable<KeyValuePair<string, object?>>) - a collection of key-value pairs to add as metric dimensions.
Version History: - v3.6 and earlier: IDictionary<string, object?> ExtractTags(Activity activity) - v3.7.0: Changed to Tags ExtractTags(Activity activity) for better performance - v3.7.1.0: Added Instrument parameter: Tags ExtractTags(Activity activity, Instrument instrument) to enable instrument-specific tag extraction
How Enrichers Work: Tag Flow Pipeline
Enrichers are called automatically when metrics are recorded. Hereβs the complete flow:
1. Activity Stops
β
2. MetricRecorder.ActivityStopped() triggered
β
3. Filter checks: ShouldRecord(activity, instrument)?
β [if true]
4. Build base TagList (operation name, status, etc.)
β
5. Enricher.ExtractTags(activity, instrument) called
β
6. Add enricher tags to TagList
β
7. instrument.Record(value, tagList)
β
8. Metric exported to backend (App Insights, Prometheus, etc.)
Example in SpanDurationMetricRecorder:
public void ActivityStopped(Activity activity)
{
Histogram<double> metric = lazyMetric.Value;
// Step 3: Apply filter
if (!(metricFilter?.ShouldRecord(activity, metric) ?? true))
return;
// Step 4: Build base tags
var tags = new TagList
{
{ "span_name", activity.OperationName },
{ "status", activity.Status.ToString() }
};
// Step 5-6: Call enricher to add additional tags
if (metricEnricher != null)
{
var enrichedTags = metricEnricher.ExtractTags(activity, metric);
foreach (var tag in enrichedTags)
{
tags.Add(tag.Key, tag.Value);
}
}
// Step 7: Record metric with all tags
metric.Record(activity.Duration.TotalMilliseconds, tags);
}OptionsBasedMetricRecordingEnricher
Configures which activity tags should become metric tags via configuration.
Configuration:
{
"OptionsBasedMetricRecordingEnricher": {
"MetricTags": [
"customer_tier",
"region",
"deployment_environment",
"service_version"
]
}
}How it works:
public virtual Tags ExtractTags(Activity activity, Instrument instrument)
{
// Gets configured tag names from options
var tagNames = options.Value.MetricTags;
// Searches activity hierarchy for matching tags
return tagNames
.Select(tagName => {
// Look in current activity and parent activities
var value = activity.GetAncestors(includeSelf: true)
.Select(a => a.GetTagItem(tagName))
.FirstOrDefault(v => v != null);
return new KeyValuePair<string, object?>(tagName, value);
})
.Where(tag => tag.Value != null);
}Example: Tag Extraction Flow
// Step 1: Create activity with tags
using var activity = ActivitySource.StartRichActivity("ProcessOrder", new
{
customer_tier = "premium", // β
Will become metric tag (in config)
region = "us-east", // β
Will become metric tag (in config)
order_id = "12345", // β Not in config, won't be included
order_value = 249.99 // β Not in config, won't be included
});
// ... business logic executes ...
// Step 2: Activity stops, enricher extracts tags
// ExtractTags is called automatically by SpanDurationMetricRecorder
// Returns: [("customer_tier", "premium"), ("region", "us-east")]
// Step 3: Tags added to metric
// Resulting metric exported:
// diginsight.span_duration{
// span_name="ProcessOrder",
// customer_tier="premium",
// region="us-east",
// status="Ok"
// } = 150msKey Benefits: - β Configuration-driven: Change tags without code changes - β Hierarchy search: Finds tags in parent activities too - β Null-safe: Only includes tags that have values - β Low cardinality: You control which tags are included
Registration:
builder.Services.AddSingleton<IMetricRecordingEnricher, OptionsBasedMetricRecordingEnricher>();Custom Enricher Examples
Create custom enrichers for advanced scenarios beyond configuration-based tagging.
Example 1: Business Context Enricher
public class BusinessContextEnricher : IMetricRecordingEnricher
{
public Tags ExtractTags(Activity activity, Instrument instrument)
{
var tags = new List<KeyValuePair<string, object?>>();
// Add deployment context from environment
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
if (environment != null)
{
tags.Add(new KeyValuePair<string, object?>("environment", environment));
}
// Add version information
var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString();
if (version != null)
{
tags.Add(new KeyValuePair<string, object?>("version", version));
}
// Bucket high-cardinality values to maintain low cardinality
if (activity.GetTagItem("order_value") is double value)
{
var bucket = value switch
{
< 50 => "small",
< 200 => "medium",
< 1000 => "large",
_ => "enterprise"
};
tags.Add(new KeyValuePair<string, object?>("order_value_bucket", bucket));
}
return tags;
}
}Example 2: Instrument-Specific Enricher
public class InstrumentAwareEnricher : IMetricRecordingEnricher
{
public Tags ExtractTags(Activity activity, Instrument instrument)
{
var tags = new List<KeyValuePair<string, object?>>();
// Add different tags based on which metric is being recorded
switch (instrument.Name)
{
case "diginsight.span_duration":
// For duration metrics, add performance-related tags
tags.Add(new KeyValuePair<string, object?>("operation_type",
activity.GetTagItem("operation_type")));
break;
case "diginsight.query_cost":
// For query cost metrics, add database-related tags
tags.Add(new KeyValuePair<string, object?>("database",
activity.GetTagItem("db.name")));
tags.Add(new KeyValuePair<string, object?>("collection",
activity.GetTagItem("db.cosmosdb.container")));
break;
case "http.request.size":
// For HTTP metrics, add route information
tags.Add(new KeyValuePair<string, object?>("http.route",
activity.GetTagItem("http.route")));
break;
}
return tags;
}
}Example 3: Complete Custom Metric with Enricher
Hereβs a complete example showing how to use enrichers in a custom metric recorder:
public class CosmosQueryCostRecorder : IActivityListenerLogic
{
private readonly Lazy<Histogram<double>> lazyMetric;
private readonly IMetricRecordingFilter? metricFilter;
private readonly IMetricRecordingEnricher? metricEnricher;
public CosmosQueryCostRecorder(
IClassAwareOptions<DiginsightActivitiesOptions> activitiesOptions,
IMeterFactory meterFactory,
IServiceProvider serviceProvider)
{
string metricName = "diginsight.query_cost";
this.lazyMetric = new Lazy<Histogram<double>>(() =>
{
var meter = meterFactory.Create(activitiesOptions.CurrentValue.MeterName);
return meter.CreateHistogram<double>(
metricName,
"RU",
"Cosmos DB query cost in Request Units");
});
// Get named enricher for this specific metric, or fall back to default
this.metricEnricher = serviceProvider.GetNamedService<IMetricRecordingEnricher>(metricName)
?? serviceProvider.GetService<IMetricRecordingEnricher>();
this.metricFilter = serviceProvider.GetNamedService<IMetricRecordingFilter>(metricName)
?? serviceProvider.GetService<IMetricRecordingFilter>();
}
public void ActivityStopped(Activity activity)
{
// Only record for CosmosDB operations that have a query cost
if (activity.GetTagItem("db.cosmosdb.request_charge") is not double cost || cost <= 0)
return;
Histogram<double> metric = lazyMetric.Value;
// Apply filter
if (!(metricFilter?.ShouldRecord(activity, metric) ?? true))
return;
// Build base tags
var tags = new TagList
{
{ "operation", activity.GetTagItem("db.operation") },
{ "database", activity.GetTagItem("db.name") },
{ "container", activity.GetTagItem("db.cosmosdb.container") }
};
// β
Call enricher to add additional tags
if (metricEnricher != null)
{
var enrichedTags = metricEnricher.ExtractTags(activity, metric);
foreach (var tag in enrichedTags)
{
tags.Add(tag.Key, tag.Value);
}
}
// Record metric with all tags
metric.Record(cost, tags);
}
}Usage:
// Register the enricher
builder.Services.AddSingleton<IMetricRecordingEnricher, InstrumentAwareEnricher>();
// Register the custom recorder
builder.Services.AddSingleton<IActivityListenerLogic, CosmosQueryCostRecorder>();
// Configure OpenTelemetry to export the metrics
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => metrics
.AddMeter("MyApp.Telemetry") // Your configured MeterName
.AddPrometheusExporter());Resulting metrics:
diginsight.query_cost{
operation="Query",
database="OrdersDB",
container="Orders",
environment="Production",
version="1.2.3"
} = 12.5 RU
Custom Metric Recorders
Beyond span duration, you can create custom MetricRecorders for specialized metrics.
Example: HTTP Payload Size Recorder
public class HttpPayloadSizeRecorder : IActivityListenerLogic
{
private readonly Histogram<long> requestSizeHistogram;
private readonly Histogram<long> responseSizeHistogram;
public HttpPayloadSizeRecorder(IMeterFactory meterFactory)
{
var meter = meterFactory.Create("MyApp.Http");
requestSizeHistogram = meter.CreateHistogram<long>("http.request.size", "bytes");
responseSizeHistogram = meter.CreateHistogram<long>("http.response.size", "bytes");
}
public void ActivityStopped(Activity activity)
{
if (activity.Source.Name != "Microsoft.AspNetCore") return;
var requestSize = activity.GetTagItem("http.request.body.size") as long? ?? 0;
var responseSize = activity.GetTagItem("http.response.body.size") as long? ?? 0;
var tags = new[]
{
new KeyValuePair<string, object?>("http.route", activity.GetTagItem("http.route")),
new KeyValuePair<string, object?>("http.status_code", activity.GetTagItem("http.status_code"))
};
requestSizeHistogram.Record(requestSize, tags);
responseSizeHistogram.Record(responseSize, tags);
}
}Example: Database Query Cost Recorder
public class DatabaseCostRecorder : IActivityListenerLogic
{
private readonly Histogram<double> queryCostHistogram;
public DatabaseCostRecorder(IMeterFactory meterFactory)
{
var meter = meterFactory.Create("MyApp.Database");
queryCostHistogram = meter.CreateHistogram<double>("database.query.cost", "RU");
}
public void ActivityStopped(Activity activity)
{
if (!activity.Source.Name.StartsWith("Azure.Cosmos")) return;
// Extract Request Units (RU) from Cosmos DB activity
if (activity.GetTagItem("db.cosmosdb.request_charge") is double requestCharge)
{
var tags = new[]
{
new KeyValuePair<string, object?>("db.operation", activity.GetTagItem("db.operation")),
new KeyValuePair<string, object?>("db.name", activity.GetTagItem("db.name"))
};
queryCostHistogram.Record(requestCharge, tags);
}
}
}Registration:
services.AddSingleton<IActivityListenerLogic, HttpPayloadSizeRecorder>();
services.AddSingleton<IActivityListenerLogic, DatabaseCostRecorder>();Complete Configuration Example
appsettings.json:
{
"Diginsight": {
"Activities": {
// β
SHARED CONFIGURATION - applies to all metrics
"MeterName": "MyApp.Telemetry",
"LogBehavior": "Show",
"LogLevel": "Debug",
"ActivitySources": {
"MyApp.*": true,
"Azure.Cosmos.*": true,
"Microsoft.EntityFrameworkCore": true,
"Microsoft.AspNetCore": false,
"System.Net.Http": true
},
"LoggedActivityNames": {
"SmartCache.OnEvicted": "Hide",
"Expensive.Debug.Operation": "Hide"
},
// β
SPAN DURATION METRIC - specific configuration
"RecordSpanDuration": true,
"SpanDurationMetricName": "diginsight.span_duration",
"SpanDurationMetricDescription": "Duration of application spans",
// β
DEFAULT FILTERS - applies to all metrics unless overridden
"SpanMeasuredActivityNames": {
"*": true
},
// β
METRIC-SPECIFIC FILTERS - override defaults per metric
"MetricSpecificSpanMeasuredActivityNames": [
{
"MetricName": "diginsight.span_duration",
"ActivityNames": {
"MyApp.Orders.*": true,
"MyApp.Payment.*": true,
"MyApp.Inventory.*": true,
"System.*": false
}
}
],
// β
DEFAULT ENRICHER TAGS - applies to all metrics
"MetricTags": [
"category_name",
"user_company"
],
// β
METRIC-SPECIFIC TAGS - additional tags per metric
"MetricSpecificTags": [
{
"MetricName": "diginsight.span_duration",
"MetricTags": [
"response_status",
"region",
"deployment_environment"
]
}
]
}
},
// Legacy filter configuration (still supported)
"OptionsBasedMetricRecordingFilter": {
"ActivityNames": {
"MyApp.Orders.*": true,
"MyApp.Payment.*": true
}
},
// Legacy enricher configuration (still supported)
"OptionsBasedMetricRecordingEnricher": {
"MetricTags": [
"customer_tier",
"region"
]
},
"OpenTelemetry": {
"Metrics": {
"ExportIntervalMilliseconds": 5000,
"MaxExportBatchSize": 512
}
}
}Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Add Diginsight with metrics
builder.Services.AddDiginsightCore();
builder.Services.AddSpanDurationMetricRecorder();
// Add filters and enrichers
builder.Services.AddSingleton<IMetricRecordingFilter, OptionsBasedMetricRecordingFilter>();
builder.Services.AddSingleton<IMetricRecordingFilter, HttpHeadersSpanDurationMetricRecordingFilter>();
builder.Services.AddSingleton<IMetricRecordingEnricher, OptionsBasedMetricRecordingEnricher>();
// Add custom metric recorders
builder.Services.AddSingleton<IActivityListenerLogic, HttpPayloadSizeRecorder>();
builder.Services.AddSingleton<IActivityListenerLogic, DatabaseCostRecorder>();
// Configure OpenTelemetry
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => metrics
.AddMeter("MyApp.*")
.AddMeter("Diginsight.Diagnostics")
.AddRuntimeInstrumentation()
.AddHttpClientInstrumentation()
.AddAspNetCoreInstrumentation()
.AddPrometheusExporter()
.AddApplicationInsightsExporter())
.WithTracing(tracing => tracing
.AddSource("MyApp.*")
.AddSource("Diginsight.Diagnostics")
.AddHttpClientInstrumentation()
.AddAspNetCoreInstrumentation()
.AddApplicationInsightsExporter());
var app = builder.Build();
// Enable Prometheus scraping endpoint
app.UseOpenTelemetryPrometheusScrapingEndpoint();
app.Run();Application Code:
public class OrderService
{
private static readonly ActivitySource ActivitySource = new("MyApp.Orders");
public async Task<Order> ProcessOrderAsync(CreateOrderRequest request)
{
using var activity = ActivitySource.StartRichActivity("ProcessOrder", new
{
customer_id = request.CustomerId,
customer_tier = request.CustomerTier, // Will be enriched as tag
region = request.Region, // Will be enriched as tag
item_count = request.Items.Count
});
try
{
var order = await ValidateAndCreateOrder(request);
activity?.SetOutput(new { order.Id, order.Status });
return order;
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
throw;
}
}
}Resulting metrics:
# Span duration with enriched tags
operation.duration{
span_name="ProcessOrder",
customer_tier="premium",
region="us-east",
status="Ok"
} = 250ms
# HTTP payload sizes
http.request.size{http.route="/api/orders", http.status_code="200"} = 1024 bytes
http.response.size{http.route="/api/orders", http.status_code="200"} = 4096 bytes
# Database costs
database.query.cost{db.operation="Query", db.name="OrdersDB"} = 12.5 RU
References
Official Documentation
OpenTelemetry Metrics Specification
The official OpenTelemetry metrics specification. Essential for understanding metric types (Counter, Histogram, Gauge), semantic conventions, and best practices for metric instrumentation..NET Metrics API
Microsoftβs documentation on System.Diagnostics.Metrics namespace. Covers Meter, Counter, Histogram creation and how Diginsight integrates with the native .NET metrics system.OpenTelemetry .NET SDK
Official OpenTelemetry .NET implementation. Shows how to configure MeterProviders, exporters (Prometheus, OTLP, Application Insights), and metric aggregation.
Monitoring Backends
Azure Application Insights Metrics
Guide to querying and visualizing metrics in Application Insights. Explains how Diginsight metrics appear in Azure Monitor and how to create dashboards.Prometheus Querying
Prometheus query language (PromQL) basics. Essential for creating alerts and dashboards from Diginsight metrics exported to Prometheus.
Best Practices
High Cardinality Metrics Problem
Excellent explanation of why high cardinality tags cause storage and performance issues. Critical reading for understanding why tag design matters in production.OpenTelemetry Semantic Conventions
Standard attribute naming conventions for metrics. Following these conventions ensures consistency and interoperability when metrics are sent to various backends.Metric Naming Best Practices
Industry standard for metric naming patterns. Helps design clear, consistent metric names that work well across different monitoring systems.
Diginsight Resources
Diginsight GitHub Repository
Official Diginsight repository with source code, samples, and documentation. Contains working examples of metric recorders, filters, and enrichers.Diginsight Samples
Real-world sample applications demonstrating metric configuration, custom recorders, and integration with various backends.